﻿namespace JadeX.HtmlMarkupMinifier.Filters
{
    using System;
    using System.IO;
    using System.Text;
    using System.Web.Mvc;

    using JadeX.HtmlMarkupMinifier.Models;

    using Orchard;
    using Orchard.Caching;
    using Orchard.ContentManagement;
    using Orchard.Logging;
    using Orchard.Mvc.Filters;
    using Orchard.UI.Admin;

    using WebMarkupMin.Core.Minifiers;
    using WebMarkupMin.Core.Settings;

    public class HtmlFilter : FilterProvider, IActionFilter
    {
        private readonly WorkContext workContext;

        private readonly ICacheManager cacheManager;

        private readonly ISignals signals;

        public HtmlFilter(IWorkContextAccessor wca, ICacheManager cacheManager, ISignals signals)
        {
            this.workContext = wca.GetContext();
            this.cacheManager = cacheManager;
            this.signals = signals;
        }

        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var settings = this.cacheManager.Get(
                "MinificationSettingsPart",
                context =>
                {
                    context.Monitor(signals.When(MinificationSettingsPart.CacheKey));
                    return workContext.CurrentSite.As<MinificationSettingsPart>();
                });

            var isAdminPage = AdminFilter.IsApplied(filterContext.RequestContext);
            var isIgnoredUrl = this.IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, settings.IgnoredUrls);
            var adminExcluded = settings.ExcludeAdmin && isAdminPage;
            var authenticatedExcluded = this.workContext.CurrentUser != null && settings.ExcludeAuthenticated && !isAdminPage;

            if (filterContext.HttpContext.Response.Filter == null ||
                adminExcluded ||
                authenticatedExcluded ||
                isIgnoredUrl)
            {
                return;
            }

            filterContext.HttpContext.Response.Filter = new TidyHtml(filterContext.HttpContext.Response.Filter, filterContext.HttpContext.Response.Output.Encoding, settings);
        }

        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
        }

        private bool IsIgnoredUrl(string url, string ignoredUrls)
        {
            if (string.IsNullOrEmpty(ignoredUrls))
            {
                return false;
            }

            url = url.TrimStart('~');

            using (var urlReader = new StringReader(ignoredUrls))
            {
                string relativePath;
                while ((relativePath = urlReader.ReadLine()) != null)
                {
                    relativePath = relativePath.TrimStart('~');

                    if (string.IsNullOrWhiteSpace(relativePath))
                    {
                        continue;
                    }

                    relativePath = relativePath.Trim();

                    // Ignore comments
                    if (relativePath.StartsWith("#"))
                    {
                        continue;
                    }

                    if (string.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
            }

            return false;
        }
    }

    internal class TidyHtml : MemoryStream
    {
        private readonly Stream stream;

        private readonly Encoding encoding;

        private readonly MinificationSettingsPart settings;

        private string html;

        public TidyHtml(Stream filter, Encoding encoding, MinificationSettingsPart settings)
        {
            this.Logger = NullLogger.Instance;
            this.stream = filter;
            this.encoding = encoding;
            this.settings = settings;
        }

        public ILogger Logger { get; set; }

        public override void Write(byte[] buffer, int offset, int count)
        {
            this.html += this.encoding.GetString(buffer);
        }

        public override void Flush()
        {
            var htmlMinifier = new HtmlMinifier(new HtmlMinificationSettings
            {
                WhitespaceMinificationMode = this.settings.WhitespaceMinificationMode,
                RemoveHtmlComments = this.settings.RemoveHtmlComments,
                RemoveHtmlCommentsFromScriptsAndStyles = this.settings.RemoveHtmlCommentsFromScriptsAndStyles,
                RemoveCdataSectionsFromScriptsAndStyles = this.settings.RemoveCdataSectionsFromScriptsAndStyles,
                UseShortDoctype = this.settings.UseShortDoctype,
                UseMetaCharsetTag = this.settings.UseMetaCharsetTag,
                EmptyTagRenderMode = this.settings.EmptyTagRenderMode,
                RemoveOptionalEndTags = this.settings.RemoveOptionalEndTags,
                RemoveTagsWithoutContent = this.settings.RemoveTagsWithoutContent,
                CollapseBooleanAttributes = this.settings.CollapseBooleanAttributes,
                RemoveEmptyAttributes = this.settings.RemoveEmptyAttributes,
                AttributeQuotesRemovalMode = this.settings.AttributeQuotesRemovalMode,
                RemoveRedundantAttributes = this.settings.RemoveRedundantAttributes,
                RemoveJsTypeAttributes = this.settings.RemoveJsTypeAttributes,
                RemoveCssTypeAttributes = this.settings.RemoveCssTypeAttributes,
                RemoveHttpProtocolFromAttributes = this.settings.RemoveHttpProtocolFromAttributes,
                RemoveHttpsProtocolFromAttributes = this.settings.RemoveHttpsProtocolFromAttributes,
                RemoveJsProtocolFromAttributes = this.settings.RemoveJsProtocolFromAttributes,
                MinifyEmbeddedCssCode = this.settings.MinifyEmbeddedCssCode,
                MinifyInlineCssCode = this.settings.MinifyInlineCssCode,
                MinifyEmbeddedJsCode = this.settings.MinifyEmbeddedJsCode,
                MinifyInlineJsCode = this.settings.MinifyInlineJsCode
            });

            var result = htmlMinifier.Minify(this.html);

            if (result.Errors.Count == 0)
            {
                if (result.Warnings.Count > 0 && this.settings.HaltOnWarnings)
                {
                    foreach (var warning in result.Warnings)
                    {
                        this.Logger.Warning(warning.Message);
                    }

                    // Undesired minification status, restore original HTML
                    this.stream.Write(this.encoding.GetBytes(this.html), 0, this.encoding.GetByteCount(this.html));
                    return;
                }

                this.stream.Write(this.encoding.GetBytes(result.MinifiedContent), 0, this.encoding.GetByteCount(result.MinifiedContent));
            }
            else
            {
                foreach (var error in result.Errors)
                {
                    this.Logger.Error(error.Message);
                }
                this.stream.Write(this.encoding.GetBytes(this.html), 0, this.encoding.GetByteCount(this.html));
            }
        }
    }
}